/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.gallery3d.glrenderer;

import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.opengl.GLUtils;
import junit.framework.Assert;

import java.util.HashMap;

import javax.microedition.khronos.opengles.GL11;

import com.android.gallery3d.app.Log;

// UploadedTextures use a Bitmap for the content of the texture.
//
// Subclasses should implement onGetBitmap() to provide the Bitmap and
// implement onFreeBitmap(mBitmap) which will be called when the Bitmap
// is not needed anymore.
//
// isContentValid() is meaningful only when the isLoaded() returns true.
// It means whether the content needs to be updated.
//
// The user of this class should call recycle() when the texture is not
// needed anymore.
//
// By default an UploadedTexture is opaque (so it can be drawn faster without
// blending). The user or subclass can override it using setOpaque().
public abstract class UploadedTexture extends BasicTexture {

	// To prevent keeping allocation the borders, we store those used borders
	// here.
	// Since the length will be power of two, it won't use too much memory.
	private static HashMap<BorderKey, Bitmap> sBorderLines = new HashMap<BorderKey, Bitmap>();
	private static BorderKey sBorderKey = new BorderKey();

	private static final String TAG = UploadedTexture.class.getSimpleName();
	private boolean mContentValid = true;

	// indicate this textures is being uploaded in background
	private boolean mIsUploading = false;
	private boolean mOpaque = true;
	private boolean mThrottled = false;
	private static int sUploadedCount;
	private static final int UPLOAD_LIMIT = 100;

	protected Bitmap mBitmap;
	private int mBorder;

	protected UploadedTexture() {
		this(false);
	}

	protected UploadedTexture(boolean hasBorder) {
		super(null, 0, STATE_UNLOADED);
		if (hasBorder) {
			setBorder(true);
			mBorder = 1;
		}
	}

	protected void setIsUploading(boolean uploading) {
		mIsUploading = uploading;
	}

	public boolean isUploading() {
		return mIsUploading;
	}

	private static class BorderKey implements Cloneable {
		public boolean vertical;
		public Config config;
		public int length;

		@Override
		public int hashCode() {
			int x = config.hashCode() ^ length;
			return vertical ? x : -x;
		}

		@Override
		public boolean equals(Object object) {
			if (!(object instanceof BorderKey))
				return false;
			BorderKey o = (BorderKey) object;
			return vertical == o.vertical && config == o.config
					&& length == o.length;
		}

		@Override
		public BorderKey clone() {
			try {
				return (BorderKey) super.clone();
			} catch (CloneNotSupportedException e) {
				throw new AssertionError(e);
			}
		}
	}

	protected void setThrottled(boolean throttled) {
		mThrottled = throttled;
	}

	private static Bitmap getBorderLine(boolean vertical, Config config,
			int length) {
		Log.d(TAG, "getBorderLine");
		BorderKey key = sBorderKey;
		key.vertical = vertical;
		key.config = config;
		key.length = length;
		Bitmap bitmap = sBorderLines.get(key);
		if (bitmap == null) {
			bitmap = vertical ? Bitmap.createBitmap(1, length, config) : Bitmap
					.createBitmap(length, 1, config);
			sBorderLines.put(key.clone(), bitmap);
		}
		return bitmap;
	}

	private Bitmap getBitmap() {
		if (mBitmap == null) {
			mBitmap = onGetBitmap();
			int w = mBitmap.getWidth() + mBorder * 2;
			int h = mBitmap.getHeight() + mBorder * 2;
			if (mWidth == UNSPECIFIED) {
				setSize(w, h);
			}
		}
		return mBitmap;
	}

	private void freeBitmap() {
		Assert.assertTrue(mBitmap != null);
		onFreeBitmap(mBitmap);
		mBitmap = null;
	}

	@Override
	public int getWidth() {
		if (mWidth == UNSPECIFIED)
			getBitmap();
		return mWidth;
	}

	@Override
	public int getHeight() {
		if (mWidth == UNSPECIFIED)
			getBitmap();
		return mHeight;
	}

	protected abstract Bitmap onGetBitmap();

	protected abstract void onFreeBitmap(Bitmap bitmap);

	protected void invalidateContent() {
		if (mBitmap != null)
			freeBitmap();
		mContentValid = false;
		mWidth = UNSPECIFIED;
		mHeight = UNSPECIFIED;
	}

	/**
	 * Whether the content on GPU is valid.
	 */
	public boolean isContentValid() {
		return isLoaded() && mContentValid;
	}

	/**
	 * Updates the content on GPU's memory.
	 * 
	 * @param canvas
	 */
	public void updateContent(GLCanvas canvas) {
		if (!isLoaded()) {
			if (mThrottled && ++sUploadedCount > UPLOAD_LIMIT) {
				return;
			}
			uploadToCanvas(canvas);
		} else if (!mContentValid) {
			Bitmap bitmap = getBitmap();
			int format = GLUtils.getInternalFormat(bitmap);
			int type = GLUtils.getType(bitmap);
			canvas.texSubImage2D(this, mBorder, mBorder, bitmap, format, type);
			freeBitmap();
			mContentValid = true;
		}
	}

	public static void resetUploadLimit() {
		sUploadedCount = 0;
	}

	public static boolean uploadLimitReached() {
		return sUploadedCount > UPLOAD_LIMIT;
	}

	private void uploadToCanvas(GLCanvas canvas) {

		Bitmap bitmap = getBitmap();
		if (bitmap != null) {
			try {
				int bWidth = bitmap.getWidth();
				int bHeight = bitmap.getHeight();
				int width = bWidth + mBorder * 2;
				int height = bHeight + mBorder * 2;
				int texWidth = getTextureWidth();
				int texHeight = getTextureHeight();

				Assert.assertTrue(bWidth <= texWidth && bHeight <= texHeight);

				// Upload the bitmap to a new texture.
				mId = canvas.getGLId().generateTexture();
				canvas.setTextureParameters(this);

				if (bWidth == texWidth && bHeight == texHeight) {
					canvas.initializeTexture(this, bitmap);
				} else {
					int format = GLUtils.getInternalFormat(bitmap);
					int type = GLUtils.getType(bitmap);
					Config config = bitmap.getConfig();

					canvas.initializeTextureSize(this, format, type);
					canvas.texSubImage2D(this, mBorder, mBorder, bitmap,
							format, type);

					if (mBorder > 0) {
						// Left border
						Bitmap line = getBorderLine(true, config, texHeight);
						canvas.texSubImage2D(this, 0, 0, line, format, type);

						// Top border
						line = getBorderLine(false, config, texWidth);
						canvas.texSubImage2D(this, 0, 0, line, format, type);
					}

					// Right border
					if (mBorder + bWidth < texWidth) {
						Bitmap line = getBorderLine(true, config, texHeight);
						canvas.texSubImage2D(this, mBorder + bWidth, 0, line,
								format, type);
					}

					// Bottom border
					if (mBorder + bHeight < texHeight) {
						Bitmap line = getBorderLine(false, config, texWidth);
						canvas.texSubImage2D(this, 0, mBorder + bHeight, line,
								format, type);
					}
				}
			} finally {
				freeBitmap();
			}
			// Update texture state.
			setAssociatedCanvas(canvas);
			mState = STATE_LOADED;
			mContentValid = true;
		} else {
			mState = STATE_ERROR;
			throw new RuntimeException("Texture load fail, no bitmap");
		}
	}

	@Override
	protected boolean onBind(GLCanvas canvas) {
		updateContent(canvas);
		return isContentValid();
	}

	@Override
	protected int getTarget() {
		return GL11.GL_TEXTURE_2D;
	}

	public void setOpaque(boolean isOpaque) {
		mOpaque = isOpaque;
	}

	@Override
	public boolean isOpaque() {
		return mOpaque;
	}

	@Override
	public void recycle() {
		super.recycle();
		if (mBitmap != null)
			freeBitmap();
	}
}
